﻿#region " LICENSE "
// DRILL - Dependency Resolution Interface Layer Library
// Copyright (c) 2008-2012 Wade A. Reynolds. All rights reserved.
// http://drill.codeplex.com/
// 
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// 
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
#endregion
using System;
using System.Reflection;
using Drill.Core.Utilities;

namespace Drill.Core
{
	/// <summary>
	/// Creates an instance of the specified type or the type factory.
	/// </summary>
	/// <typeparam name="TObjectType">The type that may be created.</typeparam>
	/// <typeparam name="TFactoryType">The factory that may be created.</typeparam>
	/// <typeparam name="TInitializationContext">
	/// The type of initialization context passed to the constructor of the new instance.
	/// </typeparam>
	public class ObjectOrFactoryActivator<TObjectType, TFactoryType, TInitializationContext>
		where TObjectType : class
		where TFactoryType : class
		where TInitializationContext : class
	{
		/// <summary>
		/// Creates an instance of <typeparamref name="TObjectType"/> or <typeparamref name="TFactoryType"/>.
		/// </summary>
		/// <param name="assemblyQualifiedTypeName">
		/// The assembly qualified type name of the type to be created which must represent 
		/// either <typeparamref name="TObjectType"/> or <typeparamref name="TFactoryType"/>.
		/// </param>
		/// <param name="initializationContext">
		/// An instance of <typeparamref name="TInitializationContext"/> that is passed to the 
		/// constructor of the created type or null if the parameterless constructor should be used.
		/// </param>
		/// <returns></returns>
		/// <exception cref="System.ArgumentNullException">assemblyQualifiedTypeName</exception>
		public Tuple<TObjectType, TFactoryType> CreateInstance(string assemblyQualifiedTypeName, TInitializationContext initializationContext = null)
		{
			if (string.IsNullOrWhiteSpace(assemblyQualifiedTypeName))
			{
				throw new ArgumentNullException("assemblyQualifiedTypeName");
			}

			Type type = GetTypeToActivate(assemblyQualifiedTypeName);

			bool isFactory = ShouldActivateAsFactoryType(type);

			TFactoryType factory;
			TObjectType obj;

			if (isFactory)
			{
				factory = CreateInstance<TFactoryType>(type, "factory", initializationContext);
				obj = null;
			}
			else
			{
				factory = null;
				obj = CreateInstance<TObjectType>(type, "object", initializationContext);
			}

			return new Tuple<TObjectType, TFactoryType>(obj, factory);
		}

		private static Type GetTypeToActivate(string assemblyQualifiedTypeName)
		{
			string assemblyName;
			string typeName;
			if (false == AssemblyUtility.TrySplitAssemblyQualifiedTypeName(assemblyQualifiedTypeName, out assemblyName, out typeName))
			{
				throw new ArgumentException(
					string.Format(
						"The assemblyQualifiedTypeName '{0}' is not formatted as a proper assembly-qualified type string. "
					  + "The format must be \"<FullNamespaceAndTypeName>, <AssemblyName>\"."
					  , assemblyQualifiedTypeName
						)
				  , "assemblyQualifiedTypeName"
					);
			}

			Assembly assembly = AssemblyUtility.EnsureAssemblyIsLoaded(new AssemblyName(assemblyName));
			if (null == assembly)
			{
				throw new DependencyResolverException("The assembly with the name '{0}' could not be loaded.", assemblyName);
			}

			Type type = assembly.GetType(typeName, false, true);
			if (null == type)
			{
				throw new DependencyResolverException(
					"The type with the name '{0}' could not be found in the '{1}' assembly."
				  , typeName
				  , assemblyName
					);
			}

			return type;
		}

		private static bool ShouldActivateAsFactoryType(Type type)
		{
			bool isFactory = typeof(TFactoryType).IsAssignableFrom(type);
			bool isObject = typeof(TObjectType).IsAssignableFrom(type);

			if (false == isFactory && false == isObject)
			{
				throw new DependencyResolverException(
					"The type '{0}' is not a factory type '{1}' and is not an object type '{2}' "
				  + "therefore the type is not valid and will not be activated."
				  , type.FullName
				  , typeof(TFactoryType).FullName
				  , typeof(TObjectType).FullName
					);
			}

			// If for some reason isFactory and isObject are both true, the factory takes precedence.
			return isFactory;
		}

		private static T CreateInstance<T>(Type type, string typeLabel, TInitializationContext activationContext)
			where T : class
		{
			bool useConstructorContextArg =
				(null != activationContext) && ShouldActivateUsingConstructorContextArgument(type);

			object[] constructorArgs =
				useConstructorContextArg
					? new object[] { activationContext }
					: new object[0];

			return AssemblyUtility.CreateInstance<T>(type, typeLabel, constructorArgs);
		}

		private static bool ShouldActivateUsingConstructorContextArgument(Type type)
		{
			// Detemrine if the type has a constructor that supports the initializationContext parameter 
			// of type TActivationContext.

			ConstructorInfo constructorInfo;
			try
			{
				constructorInfo = type.GetConstructor(new[] { typeof(TInitializationContext) });
			}
			catch
			{
				// Ignore exceptions here.
				constructorInfo = null;
			}

			// true if not null means we found the ctor that takes an initializationContext parameter.
			return (null != constructorInfo);
		}
	}
}